import abc
import contextlib
import inspect
from collections import deque
from collections.abc import Callable, Iterable, Iterator
from contextvars import ContextVar
from inspect import Parameter
from typing import Any, Mapping, Self, TypeVar

from zkl_pyutils_registries import ListRegistry, MutableCallablesRegistry, SubTypehintMatcher, TolerantMatcher, UNSET

AnyValue = TypeVar("AnyValue")
AnyInstance = TypeVar("AnyInstance")
Empty = object()


class Ioc:
    def __init__(self):
        matcher = SubTypehintMatcher()
        matcher = TolerantMatcher(matcher)
        registry = ListRegistry(matcher)
        registry = MutableCallablesRegistry(registry)
        self._registry = registry
        self._register_constructor(Ioc, lambda ioc, query, source: ioc)
        self._register_constructor(type(None), lambda ioc, query, source: None)

    def register_instance(self,
        instance: AnyInstance,
    ) -> Callable[[], AnyInstance]:
        constructor = lambda ioc, query, source: instance
        self._register_constructor(type(instance), constructor)
        return lambda: instance

    def register_type(self,
        typ: type[AnyInstance], *,
        args: Iterable[Any] | None = None,
        kwargs: Mapping[str, Any] | None = None,
        singleton: bool = True,
    ) -> Callable[[], AnyInstance]:
        constructor = lambda ioc, query, source: autowire_and_invoke_type(ioc, typ, args, kwargs, typ)
        constructor = make_singleton_constructor(constructor) if singleton else constructor
        self._register_constructor(typ, constructor)
        return lambda: constructor(self, typ, None)

    def register_factory(self,
        key: type[AnyInstance],
        factory: Callable[[...], AnyInstance], *,
        args: Iterable[Any] | None = None,
        kwargs: Mapping[str, Any] | None = None,
        singleton: bool = True,
    ) -> Callable[[], AnyInstance]:
        constructor = lambda ioc, query, source: autowire_and_invoke_callable(ioc, factory, args, kwargs, factory)
        constructor = make_singleton_constructor(constructor) if singleton else constructor
        self._register_constructor(key, constructor)
        return lambda: constructor(self, key, None)

    def _register_constructor(self,
        key: type[AnyInstance],
        constructor: Callable[[Self, type[AnyInstance] | Any, Any], AnyInstance],
    ):
        self._registry.register(key, constructor)
        if isinstance(key, type) and issubclass(key, Provider):
            provided_constructor = lambda ioc, query, source: constructor(ioc, key, None).provide(query, source)
            self._register_constructor(UNSET, provided_constructor)

    def iter_instances(self,
        query: type[AnyInstance] | Any, *,
        source: Any = None,
    ) -> Iterator[AnyInstance]:
        constructors = self._registry.match(query)
        for constructor in constructors:
            try:
                yield constructor(self, query, source)
            except NotImplementedError:
                continue

    def get_instance(self,
        query: type[AnyInstance] | Any, *,
        source: Any = None,
        required: bool = True,
        singleton: bool = True,
    ) -> AnyInstance | None:
        instances = self.iter_instances(query, source=source)
        singleton_instance = Empty
        for instance in instances:
            if not singleton:
                return instance
            if singleton_instance in (Empty, None):
                singleton_instance = instance
                continue
            if instance is None:
                continue
            if instance == singleton_instance:
                continue
            raise NotImplementedError(f"Found multiple instances for query {query}")
        if singleton_instance is Empty:
            if required:
                raise NotImplementedError(f"Found no instance for query {query}")
            singleton_instance = None
        return singleton_instance

    def scan_call_instances(self,
        query: type[AnyInstance] | Any,
        func: Callable[[AnyInstance], Any]
    ):
        with new_scan_context(self, self.iter_instances(query)) as (_, instances):
            postpone_patience = len(instances)
            while instances:
                instance = instances.popleft()
                try:
                    func(instance)
                    postpone_patience = len(instances)
                except Postpone:
                    instances.append(instance)
                    postpone_patience -= 1
                    if postpone_patience <= 0:
                        raise RuntimeError(f"Failed to call the following plugins: {instances}")

    def scan_agg_instances(self,
        query: type[AnyInstance] | Any,
        carry: AnyValue,
        func: Callable[[AnyInstance, AnyValue], AnyValue],
    ) -> AnyValue:
        def agg_func(instance: AnyInstance) -> AnyValue:
            nonlocal carry
            try:
                carry = func(instance, carry)
            except Postpone:
                pass

        self.scan_call_instances(query, agg_func)

    def ensure_after_instance(self,
        instance: AnyInstance, *,
        strict: bool = False,
    ):
        self.ensure_after_instance((instance,), strict=strict)

    def ensure_after_instances(self,
        instances: Iterable[AnyInstance], *,
        strict: bool = False,
    ):
        try:
            _, scanning_instances = next(iter_scan_contexts(self))
        except StopIteration:
            if strict:
                raise RuntimeError("Unexpected call outside of scan context!")
            return
        for instance in instances:
            if instance in scanning_instances:
                raise Postpone


# singleton

def make_singleton_constructor(constructor: Callable[[Ioc, type[AnyInstance] | Any, Any], AnyInstance]):
    value = None

    def singleton_constructor(ioc: Ioc, query: type[AnyInstance] | Any, source: Any):
        nonlocal value
        if value is None:
            value = constructor(ioc, query, source)
        return value

    return singleton_constructor


# auto wire

class Provider(abc.ABC):
    @abc.abstractmethod
    def provide(self, query: type[AnyInstance] | Any, source: Any) -> AnyInstance:
        pass


class AutoWired:
    _field_name: str

    def wire(self, instance: Any, name: str, value: Any):
        self._field_name = f"_autowired_{name}"
        setattr(instance, self._field_name, value)

    def __get__(self, instance, owner):
        if instance is None:
            return self
        return getattr(instance, self._field_name)


def autowire_and_invoke_callable(
    ioc: Ioc,
    call: Callable[[...], AnyValue],
    args: Iterable[Any] | None,
    kwargs: Mapping[str, Any] | None,
    source: Any
) -> AnyValue:
    args = [] if args is None else list(args)
    kwargs = {} if kwargs is None else dict(kwargs)

    args_ = []
    kwargs_ = {}

    signature = inspect.signature(call)
    parameters = list(signature.parameters.values())
    parameter = None

    while parameters and parameters[0].kind == Parameter.POSITIONAL_ONLY:
        parameter = parameters.pop(0)

        if args:
            value = args.pop(0)
            args_.append(value)
            continue

        if parameter.annotation is not Parameter.empty:
            try:
                value = ioc.get_instance(parameter.annotation, source=source)
                args_.append(value)
                continue
            except NotImplementedError:
                pass

        if parameter.default is not Parameter.empty:
            value = parameter.default
            args_.append(value)
            continue

        raise ValueError(f"Failed to resolve parameter {parameter.name} in {call}")

    while parameters and parameters[0].kind == Parameter.VAR_POSITIONAL:
        parameters.pop(0)

        if args:
            args_.extend(args)
            args.clear()
            continue

    while parameters and parameters[0].kind == Parameter.POSITIONAL_OR_KEYWORD:
        parameter = parameters.pop(0)

        if parameter.name in kwargs:
            if args:
                raise ValueError(f"Unexpected positional argument after {parameter.name} in {call}")
            value = kwargs.pop(parameter.name)
            kwargs_[parameter.name] = value
            continue

        if args:
            value = args.pop(0)
            args_.append(value)
            continue

        if parameter.annotation is not Parameter.empty:
            try:
                value = ioc.get_instance(parameter.annotation, source=source)
                args_.append(value)
                continue
            except NotImplementedError:
                pass

        if parameter.default is not Parameter.empty:
            value = parameter.default
            args_.append(value)
            continue

        raise ValueError(f"Failed to resolve parameter {parameter.name} in {call}")

    if args:
        if parameter is None:
            raise ValueError(f"Unexpected positional argument in {call}")
        else:
            raise ValueError(f"Unexpected positional argument after {parameter.name} in {call}")

    while parameters and parameters[0].kind == Parameter.KEYWORD_ONLY:
        parameter = parameters.pop(0)

        if parameter.name in kwargs:
            if args:
                raise ValueError(f"Unexpected positional argument after {parameter.name} in {call}")
            value = kwargs.pop(parameter.name)
            kwargs_[parameter.name] = value
            continue

        if parameter.annotation is not Parameter.empty:
            try:
                value = ioc.get_instance(parameter.annotation, source=source)
                kwargs_[parameter.name] = value
                continue
            except NotImplementedError:
                pass

        if parameter.default is not Parameter.empty:
            value = parameter.default
            kwargs_[parameter.name] = value
            continue

        raise ValueError(f"Failed to resolve parameter {parameter.name} in {call}")

    while parameters and parameters[0].kind == Parameter.VAR_KEYWORD:
        parameter = parameters.pop(0)

        if kwargs:
            kwargs_.update(kwargs)
            kwargs.clear()

    return call(*args_, **kwargs_)


def autowire_and_invoke_type(
    ioc: Ioc,
    typ: type[AnyValue],
    args: Iterable[Any] | None,
    kwargs: Mapping[str, Any] | None,
    source: Any
) -> AnyValue:
    instance = object.__new__(typ)
    annotations = inspect.get_annotations(typ)
    for name in dir(typ):
        if name.startswith('__'):
            continue
        prop = getattr(typ, name)
        if isinstance(prop, type) and issubclass(prop, AutoWired):
            prop = prop()
            setattr(typ, name, prop)
        if isinstance(prop, AutoWired):
            if name not in annotations:
                raise TypeError(f"Property {name} in {typ} has no annotation!")
            annotation = annotations[name]
            value = ioc.get_instance(annotation, source=source)
            prop.wire(instance, name, value)

    # noinspection PyTypeChecker
    autowire_and_invoke_callable(ioc, instance.__init__, args, kwargs, source)
    return instance


# scan context

class Postpone(Exception):
    pass


ScanContext = tuple[Ioc, deque[AnyInstance]]

scan_context_stack_context_var = ContextVar[deque[ScanContext] | None]('scan_context_stack', default=None)


def get_scan_context_stack() -> deque[ScanContext]:
    scan_context_stack_stack = scan_context_stack_context_var.get()
    if scan_context_stack_stack is None:
        scan_context_stack_stack = deque()
        scan_context_stack_context_var.set(scan_context_stack_stack)
    return scan_context_stack_stack


def iter_scan_contexts(ioc: Ioc | None = None) -> Iterator[ScanContext]:
    scan_context_stack = get_scan_context_stack()
    if ioc is None:
        yield from reversed(scan_context_stack)
    else:
        for scan_context in reversed(scan_context_stack):
            if scan_context[0] == ioc:
                yield scan_context


@contextlib.contextmanager
def new_scan_context(ioc: Ioc, items: Iterable[AnyInstance]):
    scan_context = ioc, deque(items)
    scan_context_stack = get_scan_context_stack()
    scan_context_stack.append(scan_context)
    try:
        yield scan_context
    finally:
        scan_context_stack.pop()
